Skip to content

Commit 640dd14

Browse files
authored
Fix escaped quotes and newlines for Android strings (#3650)
1 parent 1d59adf commit 640dd14

File tree

4 files changed

+123
-4
lines changed

4 files changed

+123
-4
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from re import compile
2+
3+
from django.db import migrations
4+
5+
6+
def fix_android_newlines(apps, schema_editor):
7+
esc_nl = compile(r"(?<!\\)\\n\s*")
8+
9+
Entity = apps.get_model("base", "Entity")
10+
android_entities = Entity.objects.filter(
11+
resource__format="xml", string__contains="\\n"
12+
)
13+
for ent in android_entities:
14+
ent.string = esc_nl.sub(r"\\n\n", ent.string)
15+
Entity.objects.bulk_update(android_entities, ["string"])
16+
17+
Translation = apps.get_model("base", "Translation")
18+
android_translations = Translation.objects.filter(
19+
entity__resource__format="xml", string__contains="\\n"
20+
)
21+
for trans in android_translations:
22+
trans.string = esc_nl.sub(r"\\n\n", trans.string)
23+
Translation.objects.bulk_update(android_translations, ["string"], batch_size=2000)
24+
25+
26+
class Migration(migrations.Migration):
27+
dependencies = [
28+
("base", "0076_remove_pontoon_intro_project"),
29+
]
30+
31+
operations = [
32+
migrations.RunPython(
33+
code=fix_android_newlines, reverse_code=migrations.RunPython.noop
34+
),
35+
]

pontoon/sync/core/translations_to_repo.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,8 @@ def set_translations(
358358
]
359359

360360

361-
android_quotes = compile(r"(?<!\\)(['\"])")
361+
android_nl = compile(r"\s*\n\s*")
362+
android_esc = compile(r"(?<!\\)\\([nt])\s*")
362363
webext_placeholder = compile(r"\$([a-zA-Z0-9_@]+)\$|(\$[1-9])|\$(\$+)")
363364

364365

@@ -395,7 +396,11 @@ def set_translation(
395396
for tx in translations:
396397
if tx.entity.key == key:
397398
if res.format == Format.android:
398-
entry.value = android_quotes.sub(r"\\\1", tx.string)
399+
# Literal newlines \n and tabs \t are included in the string
400+
entry.value = android_esc.sub(
401+
lambda m: "\n" if m[1] == "n" else "\t",
402+
android_nl.sub(" ", tx.string),
403+
)
399404
elif (
400405
res.format == Format.webext
401406
and isinstance(entry.value, PatternMessage)

pontoon/sync/formats/xml.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ def parse(res: Resource[Message]):
2828

2929

3030
esc_u = compile(r"(?<!\\)\\u[0-9]{4}")
31-
esc_char = compile(r"(?<!\\)\\(.)")
31+
esc_char = compile(r"(?<!\\)\\([^nt])")
32+
esc_nl = compile(r"(?<!\\)\\n\s*")
3233
ws_around_outer_tag = compile(r"^\s+(?=<)|(?<=>)\s+$")
3334
ws_before_block = compile(r"\s+(?=<(br|label|li|p|/?ul)\b)")
3435
ws_after_block = compile(r"((?<=<br>)|(?<=<br/>)|(?<=</ul>)|(?<=\\n))\s+")
@@ -40,6 +41,7 @@ def as_translation(order: int, entry: Entry[Message]):
4041
string = unescape(string)
4142
string = esc_u.sub(lambda m: chr(int(m[1])), string)
4243
string = esc_char.sub(r"\1", string)
44+
string = esc_nl.sub(r"\\n\n", string)
4345
string = ws_around_outer_tag.sub("", string)
4446
string = ws_before_block.sub("\n", string)
4547
string = ws_after_block.sub("\n", string)

pontoon/sync/tests/test_e2e.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@
1010

1111
from django.conf import settings
1212

13-
from pontoon.base.models import ChangedEntityLocale, TranslatedResource, Translation
13+
from pontoon.base.models import (
14+
ChangedEntityLocale,
15+
Entity,
16+
TranslatedResource,
17+
Translation,
18+
)
1419
from pontoon.base.tests import (
1520
EntityFactory,
1621
LocaleFactory,
@@ -231,6 +236,78 @@ def test_translation_before_source():
231236
assert file.read() == "a0 = New translation 0\n"
232237

233238

239+
@pytest.mark.django_db
240+
def test_android():
241+
mock_vcs = MockVersionControl(changes=None)
242+
with (
243+
TemporaryDirectory() as root,
244+
patch("pontoon.sync.core.checkout.get_repo", return_value=mock_vcs),
245+
patch("pontoon.sync.core.translations_to_repo.get_repo", return_value=mock_vcs),
246+
):
247+
# Database setup
248+
settings.MEDIA_ROOT = root
249+
locale = LocaleFactory.create(code="de-Test", name="Test German")
250+
repo_src = RepositoryFactory(
251+
url="http://example.com/src-repo", source_repo=True
252+
)
253+
repo_tgt = RepositoryFactory(url="http://example.com/tgt-repo")
254+
project = ProjectFactory.create(
255+
name="test-android",
256+
locales=[locale],
257+
repositories=[repo_src, repo_tgt],
258+
)
259+
res = ResourceFactory.create(project=project, path="strings.xml", format="xml")
260+
261+
entity = EntityFactory.create(resource=res, key="quotes", string="Prev quotes")
262+
TranslationFactory.create(
263+
entity=entity,
264+
locale=locale,
265+
string="'Hello' \"translation\"",
266+
active=True,
267+
approved=True,
268+
)
269+
270+
entity = EntityFactory.create(
271+
resource=res, key="newline", string="Prev newline"
272+
)
273+
TranslationFactory.create(
274+
entity=entity,
275+
locale=locale,
276+
string="translated \n escaped \\n newlines",
277+
active=True,
278+
approved=True,
279+
)
280+
281+
# Filesystem setup
282+
src_root = repo_src.checkout_path
283+
src_xml = """<?xml version="1.0" ?>
284+
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
285+
<string name="quotes">\\'Hello\\' \\"source\\"</string>
286+
<string name="newline">literal \n escaped \\n newlines</string>
287+
</resources>"""
288+
makedirs(src_root)
289+
build_file_tree(src_root, {"en-US": {"strings.xml": src_xml}})
290+
291+
tgt_root = repo_tgt.checkout_path
292+
makedirs(tgt_root)
293+
build_file_tree(tgt_root, {"de-Test": {}})
294+
295+
# Test
296+
sync_project_task(project.pk)
297+
assert set(ent.string for ent in Entity.objects.filter(resource=res)) == {
298+
"'Hello' \"source\"",
299+
"literal escaped \\n\nnewlines",
300+
}
301+
with open(join(repo_tgt.checkout_path, "de-Test", "strings.xml")) as file:
302+
assert file.read() == dedent("""\
303+
<?xml version="1.0" encoding="utf-8"?>
304+
<resources>
305+
<string name="quotes">\\'Hello\\' \\"translation\\"</string>
306+
<string name="newline">translated escaped \\nnewlines</string>
307+
</resources>
308+
""")
309+
310+
234311
@pytest.mark.django_db
235312
def test_fuzzy():
236313
with TemporaryDirectory() as root:

0 commit comments

Comments
 (0)